package galaxies;

import eventManager.EventManager;
import galaxies.enums.EGalaxyType;
import galaxies.enums.SpiralArmNumbers;
import galaxies.sectors.SectorBuilder;
import galaxies.sectors.SpiralSectorBuilder;
import galaxies.starRegions.StarRegionBuilder;
import galaxies.starRegions.StarRegionEllipticalBuilder;
import galaxies.starRegions.StarRegionModel;
import galaxies.starRegions.StarRegionSpiralArmModel;
import galaxies.starRegions.StarRegionSpiralBuilder;
import galaxies.starRegions.StarRegionSpiralCoreModel;
import gameAppLayer.GameSettings;
import gameAppLayer.XMLData;
import gameAppLayer.ExternalData;

import java.awt.Polygon;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

import utilities.CoordConvert;
import utilities.PointRotate;

import com.jme3.asset.AssetManager;
import com.jme3.math.Vector2f;

import controller.AbstractController;
import controller.IController;

public class GalaxyGenerator2 {

	
	private static final Logger logger = Logger.getLogger(GalaxyGenerator2.class.getName());

	private ArrayList<IController> controllers = new ArrayList<IController>();
	public ArrayList<ArrayList<Integer>> locations = new ArrayList<ArrayList<Integer>>();
	public HashMap<String, ArrayList<Integer>> offsets = new HashMap<String, ArrayList<Integer>>();
	
	//holds polygons used when arranging multiple galaxies in space
	public HashMap<String, Polygon> polys1 = new HashMap<String, Polygon>();
	
	public GalaxyGenerator2() {}
	

	public ArrayList<IController> createGalaxies(
			GameSettings settings, 
			AssetManager assets,			
			HashMap<String, ArrayList<ArrayList<Integer>>> dims, 
			HashMap<String, Double> rotations, 
			ExternalData externalData) {
		
		logger.log(Level.INFO, "Start");
		
		ArrayList<String> galaxies = settings.getGalaxyRefs();
		SpiralArmNumbers armNumbers = externalData.getArmNumbers();
		
		//arrange galaxies
		offsets = arrangeGalaxies(dims);		
		
		double rotation;
		ArrayList<Integer> thisOffset;
		double coreWidth;
		double coreSide;
		double triangleSide;
		GalaxySpiralBuilder2 factSpiral;
		HashMap<Integer, ArrayList<Integer>> galLocations;
		int numArms;
		StarRegionSpiralBuilder spiralStarRegionFact;
		ArrayList<StarRegionSpiralCoreModel> coreRegions;
		ArrayList<StarRegionSpiralArmModel> armRegions;
		SpiralSectorBuilder sectorFact;
		ArrayList<ArrayList<Integer>> mainBodyLocations;
		ArrayList<ArrayList<Integer>> haloLocations;
		int size;
		int density;
		SpiralStarLocationGenerator starLocationFact;
		ArrayList<ArrayList<Integer>> tertiaryLocations;
		ArrayList<ArrayList<Integer>> secondaryLocations;
		ArrayList<ArrayList<Integer>> starLocations;
		
		GalaxyEllipticalBuilder factElliptical;
		StarRegionEllipticalBuilder ellipticalStarRegionFact;
		
		StarRegionBuilder generalStarRegionFact;
		SectorBuilder generalSectorFact;
		
		GalaxyCustomBuilder factCustom;
		
		int largestDist;
		for (String galRef: galaxies) {//EGalaxyType type: types) {
			EGalaxyType type = settings.getGalaxyType(galRef);
			
			//NOTE:  Currently only one view for a galaxy
			switch (type) {
			case ELLIPTICAL:				
				logger.log(Level.INFO, "Create elliptical");
				
				thisOffset = offsets.get(galRef);
				
				//Create Galaxy
				factElliptical = new GalaxyEllipticalBuilder();
				controllers.addAll(factElliptical.createGalaxy(settings, assets, galRef, dims.get(galRef)));
				galLocations = factElliptical.getLocations();
				
				logger.log(Level.INFO, "GalLocations size= " + galLocations.size());
				
				//Create Star Region for building sectors
				ellipticalStarRegionFact = new StarRegionEllipticalBuilder();
				ArrayList<StarRegionModel> regions = ellipticalStarRegionFact.buildRegions(dims.get(galRef));
				
				//Simple, random galaxy rotation
				rotation = rotations.get(galRef);
				for (StarRegionModel region: regions) {
					ArrayList<Vector2f> bounds = region.getPoints();
					for (Vector2f point: bounds) {
						PointRotate.simpleRotate(rotation, point);
					}
				}
				
				//Create sectors
				generalSectorFact = new SectorBuilder(galLocations, thisOffset);
				controllers.addAll(generalSectorFact.buildSectors(regions, assets));
				ArrayList<ArrayList<Integer>> ellipLocations = generalSectorFact.chosenLocations;
				
				logger.log(Level.INFO, "Choosing star locations...");
				//Choose star locations
				density = settings.getGalaxyDensity(galRef);
				starLocationFact = new SpiralStarLocationGenerator();
				tertiaryLocations = new ArrayList<ArrayList<Integer>>();
				secondaryLocations = new ArrayList<ArrayList<Integer>>();
				
				starLocations = starLocationFact.chooseRandomLocations(
							ellipLocations, secondaryLocations, tertiaryLocations, 
							1.0, 0.0, 0.0, density, type);
				
				logger.log(Level.INFO, "Finished with star locations...");				
				locations.addAll(starLocations);				
				
				break;
			case SPIRAL:
				logger.log(Level.INFO, "Create spiral");
				
				thisOffset = offsets.get(galRef);
				
				//Create Galaxy
				factSpiral = new GalaxySpiralBuilder2();
				controllers.addAll(factSpiral.createGalaxy(settings, assets, galRef, dims.get(galRef), armNumbers));				
				galLocations = factSpiral.getLocations();
				logger.log(Level.INFO, "GalLocations size= " + galLocations.size());
				numArms = factSpiral.getNumArms();
				logger.log(Level.INFO, "Number of Arms= " + numArms);
				
				//Create Star Regions for building sectors
				spiralStarRegionFact = new StarRegionSpiralBuilder();				
				coreRegions = spiralStarRegionFact.buildCoreRegions(numArms, dims.get(galRef).get(0));
				armRegions = spiralStarRegionFact.buildArmRegions(numArms, dims.get(galRef).get(0));
				coreWidth = spiralStarRegionFact.coreWidth;
				coreSide = spiralStarRegionFact.coreSide;
				triangleSide = spiralStarRegionFact.triangleSide;
				
				//Rotate galaxy randomly, plus spiral rotate
				rotation = rotations.get(galRef);
				largestDist = (int) findLargest(dims.get(galRef));
				for (StarRegionSpiralArmModel armRegion: armRegions) {
					ArrayList<Vector2f> bounds = armRegion.getBounds();
					for (Vector2f bound: bounds) {
						PointRotate.simpleRotate(rotation, bound);
						PointRotate.spiralRotate(triangleSide, largestDist, bound);
					}
				}
				//rotate core as well...
				for (StarRegionSpiralCoreModel coreRegion: coreRegions) {
					ArrayList<Vector2f> bounds = coreRegion.getPoints();
					for (Vector2f bound: bounds) {
						PointRotate.simpleRotate(rotation, bound);
					}
				}
				
				//Create Sectors
				sectorFact = new SpiralSectorBuilder(galLocations, thisOffset);
				controllers.addAll(sectorFact.buildCoreSectors(coreRegions, triangleSide, assets));
				controllers.addAll(sectorFact.buildArmSectors(armRegions, assets));
				mainBodyLocations = sectorFact.mainTileList; 
				System.out.println(mainBodyLocations.size());
				haloLocations = sectorFact.haloTileList;
				
				//Choose star locations
				density = settings.getGalaxyDensity(galRef);
				starLocationFact = new SpiralStarLocationGenerator();
				tertiaryLocations = new ArrayList<ArrayList<Integer>>();
				
				starLocations = starLocationFact.chooseRandomLocations(
							mainBodyLocations, haloLocations, tertiaryLocations, 
							0.9, 0.1, 0.0, density, type);
				
				locations.addAll(starLocations);
				
				break;
			case BAR_SPIRAL:
				//same as regular spiral, but numArms set to 2
				logger.log(Level.INFO, "Create BarSpiral");
				
				thisOffset = offsets.get(galRef);
				
				//Create Galaxy
				factSpiral = new GalaxySpiralBuilder2();
				controllers.addAll(factSpiral.createGalaxy(settings, assets, galRef, dims.get(galRef), armNumbers));				
				galLocations = factSpiral.getLocations();
				numArms = 2; //factSpiral.getNumArms();
				logger.log(Level.INFO, "GalLocations size= " + galLocations.size());
				
				//Create Star Regions for building sectors
				spiralStarRegionFact = new StarRegionSpiralBuilder();
				coreRegions = spiralStarRegionFact.buildCoreRegions(numArms, dims.get(galRef).get(0));
				armRegions = spiralStarRegionFact.buildArmRegions(numArms, dims.get(galRef).get(0));
				coreWidth = spiralStarRegionFact.coreWidth;
				coreSide = spiralStarRegionFact.coreSide;
				triangleSide = spiralStarRegionFact.triangleSide;
				
				//Rotate galaxy randomly, plus spiral rotate
				rotation = rotations.get(galRef);
				largestDist = (int) findLargest(dims.get(galRef));
				for (StarRegionSpiralArmModel armRegion: armRegions) {
					ArrayList<Vector2f> bounds = armRegion.getBounds();
					for (Vector2f bound: bounds) {
						PointRotate.simpleRotate(rotation, bound);
						PointRotate.spiralRotate(triangleSide, largestDist, bound);
					}
				}
				//rotate core as well...
				for (StarRegionSpiralCoreModel coreRegion: coreRegions) {
					ArrayList<Vector2f> bounds = coreRegion.getPoints();
					for (Vector2f bound: bounds) {
						PointRotate.simpleRotate(rotation, bound);
					}
				}
				
				//Create Sectors
				sectorFact = new SpiralSectorBuilder(galLocations, thisOffset);
				controllers.addAll(sectorFact.buildCoreSectors(coreRegions, triangleSide, assets));
				controllers.addAll(sectorFact.buildArmSectors(armRegions, assets));
				mainBodyLocations = sectorFact.mainTileList; 
				haloLocations = sectorFact.haloTileList;
				
				//Choose star locations			
				density = settings.getGalaxyDensity(galRef);
				starLocationFact = new SpiralStarLocationGenerator();
				tertiaryLocations = new ArrayList<ArrayList<Integer>>();
				
				starLocations = starLocationFact.chooseRandomLocations(
							mainBodyLocations, haloLocations, tertiaryLocations, 
							0.9, 0.1, 0.0, density, type);
				
				locations.addAll(starLocations);
				break;
			/*case UNIFORM:
				UniformGalaxy uniform = new UniformGalaxy(currGalaxy);
				arrangeGalaxy(currGalaxy);
				uniform.buildGalaxyModel();
				break;*/
			case CUSTOM:
				logger.log(Level.INFO, "Create CustomGalaxy");
				
				thisOffset = offsets.get(galRef);
				CustomGalaxyData data = settings.getCustomGalaxyData(galRef);
				
				//Create Galaxy
				factCustom = new GalaxyCustomBuilder();
				controllers.addAll(factCustom.createGalaxy(settings, assets, galRef, dims.get(galRef), data));
				double scaleFactor = factCustom.getScaleFactor();
				galLocations = factCustom.getLocations();
				
				logger.log(Level.INFO, "GalLocations size= " + galLocations.size());
				
				//scale up dims
				double x, y;
				ArrayList<Vector2f> scalePoints = new ArrayList<Vector2f>();
				for (ArrayList<Double> point: data.getPoints()) {
					System.out.println("Starting (X,Y): " + point.get(0) + " " + point.get(1));					
					
					x = point.get(0)*scaleFactor;
					y = point.get(1)*scaleFactor;
					
					System.out.println("Scaled (X,Y): " + x + " " + y);
					
					//Convert to integers
					Vector2f vec = new Vector2f((float)x,(float)y);
					scalePoints.add(vec);
				}
				
				//Create Star Regions for building sectors
				//NOTE:  Galaxy points form one big star region.
				StarRegionModel regionModel = new StarRegionModel(scalePoints);
				ArrayList<StarRegionModel> customRegions = new ArrayList<StarRegionModel>();
				customRegions.add(regionModel);
				
				//Rotate galaxy randomly
				rotation = rotations.get(galRef);
				for (StarRegionModel region: customRegions) {
					ArrayList<Vector2f> bounds = region.getPoints();
					for (Vector2f point: bounds) {
						PointRotate.simpleRotate(rotation, point);
					}
				}
				logger.log(Level.INFO, "Galaxy rotated");
				
				//Create Sectors
				generalSectorFact = new SectorBuilder(galLocations, thisOffset);
				controllers.addAll(generalSectorFact.buildSectors(customRegions, assets));
				ArrayList<ArrayList<Integer>> customLocations = generalSectorFact.chosenLocations;
								
				logger.log(Level.INFO, "Choosing star locations...");
				//Choose star locations				
				density = settings.getGalaxyDensity(galRef);
				starLocationFact = new SpiralStarLocationGenerator();
				tertiaryLocations = new ArrayList<ArrayList<Integer>>();
				secondaryLocations = new ArrayList<ArrayList<Integer>>();
				
				starLocations = starLocationFact.chooseRandomLocations(
							customLocations, secondaryLocations, tertiaryLocations, 
							1.0, 0.0, 0.0, density, type);
				
				logger.log(Level.INFO, "Finished with star locations...");				
				locations.addAll(starLocations);				
				
				break;
			default:
				//report that galaxyType is not any registered type, error
			}						
		}		
		logger.log(Level.INFO, "GalaxyGenerator: createGalaxies() finished");
		return controllers;
	}
	
	public ArrayList<ArrayList<Integer>> getLocations() {
		return this.locations;
	}
	
	
	
	private HashMap<String, ArrayList<Integer>> arrangeGalaxies(HashMap<String, ArrayList<ArrayList<Integer>>> dims) {
		logger.log(Level.INFO, "Arranging Galaxies");
		
		HashMap<String, ArrayList<Integer>> offsets = new HashMap<String, ArrayList<Integer>>();
		
		if (dims.size()==1) {
			for (Map.Entry<String, ArrayList<ArrayList<Integer>>> entry : dims.entrySet()) {
				ArrayList<Integer> offs = new ArrayList<Integer>();
				offs.add(0);
				offs.add(0);
				offs.add(0);
				
				offsets.put(entry.getKey(), offs);
				logger.log(Level.INFO, "Only one galaxy, finishing");
			}
			return offsets;
		}
		
		logger.log(Level.INFO, "Multiple galaxies, continue to arrange");
		//create polygons out of dimensions
		ArrayList<Polygon> polys = new ArrayList<Polygon>();
		ArrayList<String> galRefs = new ArrayList<String>();
		String galRef;
		int maxX = 0, maxY = 0, minX = 0, minY = 0;
		int totalX = 0, totalY = 0;
		logger.log(Level.INFO, "Creating polygons for galaxy collision testing...");
		for (Map.Entry<String, ArrayList<ArrayList<Integer>>> entry : dims.entrySet()) {
			galRef = entry.getKey();
			Polygon poly = new Polygon();
			maxX = 0; maxY = 0; minX = 0; minY = 0;
			
			//Create fully-described polygon for collision testing			
			for (ArrayList<Integer> point: entry.getValue()) {
				if (point.get(0) > maxX)
					maxX = point.get(0);
				else if (point.get(0) < minX)
					minX = point.get(0);
				
				if (point.get(1) > maxY)
					maxY = point.get(1);
				else if (point.get(1) < minY)
					minY = point.get(1);
				
				poly.addPoint(point.get(0), point.get(1));
			}
			totalX+=maxX; //later used to keep galaxies from translating too far away
			totalY+=maxY;
			
			polys.add(poly);
			polys1.put(galRef, poly);
			galRefs.add(galRef);
		}
		
		
		
		/*
		 * Algorithm:
		 * 1) Place the first polygon near (0,0) point.
		 * 2) Choose next polygon.
		 * 3) Choose a random already-placed polygon and a random direction.
		 * 4) Step out from the random poly in the selected direction, checking for collisions.
		 * 5) When no collision, place the current poly.  Convert displacement to hexCoords, record it and record galRef
		 * 6) Repeat from (2)
		 */
		Random rand = new Random();
		double x,y;
		int sign;
		
		ArrayList<Integer> hexTranslation;// = CoordConvert.cart2HexTile(translation);
		//offsets.put(galRef, hexTranslation);//add translation to list
		
//		System.out.println("Translated " + galRef + " by (u,v,w)= " + hexTranslation.get(0) + " " 
//				+ hexTranslation.get(1) + " " + hexTranslation.get(2));
		
		logger.log(Level.INFO, "Translating...");
		//Translate all galaxies
		int xStep = 0; //randomly selected displacements
		int yStep = 0;
		double theta = 0.0; //direction to walk in
		double stepSize = 10;		
		
		ArrayList<Polygon> placedPolys = new ArrayList<Polygon>(); //holds polys to reference for new placements
		ArrayList<String> placedPolyNames = new ArrayList<String>();		
		
		Polygon selectedPoly;
		String selectedGalRef;
		boolean collisionFlag;
		for (int i = 0; i < polys.size(); i++) {
			collisionFlag = true;
			
			//select next polygon
			selectedPoly = polys.get(i);
			selectedGalRef = galRefs.get(i);
			logger.log(Level.INFO, "Selected " + selectedGalRef + " to translate");
			
			//select random direction to translate
			theta = rand.nextDouble()*2*Math.PI;
			logger.log(Level.INFO, "Direction to translate (radians)= " + theta);
			
			//Step out from reference poly in theta direction
			xStep = (int) (stepSize*Math.cos(theta)+0.5);
			yStep = (int) (stepSize*Math.sin(theta)+0.5);
			
			int cumuX = 0; //total displacement in the theta direction
			int cumuY = 0;
			
			while (collisionFlag) {
				collisionFlag = false; //assume no collision, then find one below
				
				//step
				selectedPoly.translate(xStep, yStep);
				logger.log(Level.INFO, "Translated " + selectedGalRef + " by (x,y)= " + xStep + " " + yStep);
				
				cumuX += xStep;
				cumuY += yStep;	
				
				//move back and start again if it's moved too far
				//totalX and totalY are the sum of all the maxX and maxY values above
				if ((Math.abs(cumuX) > 1.5*totalX) || (Math.abs(cumuY) > 1.5*totalY)) {
					selectedPoly.translate(-cumuX, -cumuY);
					
					//select new random direction
					theta = rand.nextDouble()*2*Math.PI;
					logger.log(Level.INFO, "Theta " + theta);
					
					cumuX = 0; //reset
					cumuY = 0;
					
					//Step out from reference poly in theta direction
					xStep = (int) (stepSize*Math.cos(theta)+0.5);
					yStep = (int) (stepSize*Math.sin(theta)+0.5);					
				}
				
				logger.log(Level.INFO, "cumuX, cumuY= " + cumuX + " " + cumuY);
				
				//Compare every other polygon to this one for collision detection
				for (Polygon referencePoly: placedPolys) {
					
					if (referencePoly.intersects(selectedPoly.getBounds2D())) {
						collisionFlag = true;
						logger.log(Level.INFO, "COLLISION!");
					}
				}
				
				if (!collisionFlag) {
					placedPolys.add(selectedPoly);
					placedPolyNames.add(selectedGalRef);
					logger.log(Level.INFO, "Galaxy " + selectedGalRef + " successfully placed");
					
				}
				else { //step further out, larger step size, continue
					//increase step size for next iteration
					xStep *= 1.5; //=xStep;//*adjustment;
					yStep *= 1.5; //=yStep;//*adjustment;
					logger.log(Level.INFO, "Galaxy not placed.  New step: " + xStep + " " + yStep);
				}
			}
			ArrayList<Double> cartTranslation = new ArrayList<Double>();
			cartTranslation.add((double) cumuX);
			cartTranslation.add((double) cumuY);
			
			hexTranslation = CoordConvert.cart2HexTile(cartTranslation);//new ArrayList<Integer>();
			
			logger.log(Level.INFO, "Translated " + selectedGalRef + " by (u,v,w)= " + hexTranslation.get(0) + " " 
					+ hexTranslation.get(1) + " " + hexTranslation.get(2));			
			
			offsets.put(selectedGalRef, hexTranslation);
		}
		logger.log(Level.INFO, "Finished arranging Galaxies");
		return offsets;
	}	
	
	
	public double findLargest(ArrayList<ArrayList<Integer>> dims) {
		double dist;
		double maxDist = 0;		
		int x, y;
		for (ArrayList<Integer> point: dims) {
			x = point.get(0);
			y = point.get(1);
			
			dist = Math.sqrt(x*x + y*y);
			if (dist > maxDist) {
				maxDist = dist;				
			}			
		}
		
		return maxDist;
	}
	
}
